﻿using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;

namespace CloudShot.Utils.Encoding
{
  public class FastAnimatedGifEncoder
  {
    private readonly Byte[] _graphicControlExtensionBlock = new Byte[8]
    {
      33, //Extension introducer
      249, //Graphic control extension
      4, //Size of block
      9, //Flags: reserved, disposal method, user input, transparent color
      9, //Delay time low byte (1/100 sec)
      0, //Delay time high byte (1/100 sec)
      255, //Transparent color index
      0 //Block terminator
    };

    private BinaryWriter _binaryWriter;
    private EncoderParameters _encoderParameters;
    private ImageCodecInfo _imageCodecInfo;
    private bool _isHeaderSet;
    private int _fps;

    public long Quality { get; set; }

    public int FPS
    {
      get { return _fps; }
      set
      {
        _fps = value;
        SetDelay(value);
      }
    }

    public FastAnimatedGifEncoder()
    {
      Quality = 0L;
      FPS = 10;
    }

    public void Start()
    {
      _binaryWriter = new BinaryWriter(new MemoryStream());

      // Create an Encoder object based on the GUID 
      // for the Quality parameter category.
      _encoderParameters = new EncoderParameters(2);
      _encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, Quality);
      _encoderParameters.Param[1] = new EncoderParameter(Encoder.Compression, (long)EncoderValue.CompressionCCITT4);
      _imageCodecInfo = GetEncoder(ImageFormat.Gif);
    }

    public void AddFrame(Image frame)
    {
      WriteFrame(frame, !_isHeaderSet);

      if (!_isHeaderSet)
        _isHeaderSet = true;
    }

    public Image Finish()
    {
      WriteImageTerminator();

      Image animatedGif = Image.FromStream(_binaryWriter.BaseStream);
      _binaryWriter.Close();
      _binaryWriter = null;
      
      return animatedGif;
    }

    private static ImageCodecInfo GetEncoder(ImageFormat format)
    {
      ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();

      return codecs.FirstOrDefault(codec => codec.FormatID == format.Guid);
    }

    private void SetDelay(int fps)
    {
      if (fps <= 0)
        throw new ArgumentException("FPS cannot be less or equal zero.", "fps");

      short delay = (short)Math.Round(100f / fps, MidpointRounding.AwayFromZero);
      _graphicControlExtensionBlock[4] = LoByte(delay);
      _graphicControlExtensionBlock[5] = HiByte(delay);
    }

    private void WriteHeaderAndColorTable(Byte[] buf1)
    {
      //only write these the first time....
      _binaryWriter.Write(buf1, 0, 781); //Header & global color table
    }

    private void WriteApplicationExtensionBlock()
    {
      var applicationExtensionBlock = new Byte[19];
      applicationExtensionBlock[0] = 33; //extension introducer
      applicationExtensionBlock[1] = 255; //application extension
      applicationExtensionBlock[2] = 11; //size of block
      applicationExtensionBlock[3] = 78; //N
      applicationExtensionBlock[4] = 69; //E
      applicationExtensionBlock[5] = 84; //T
      applicationExtensionBlock[6] = 83; //S
      applicationExtensionBlock[7] = 67; //C
      applicationExtensionBlock[8] = 65; //A
      applicationExtensionBlock[9] = 80; //P
      applicationExtensionBlock[10] = 69; //E
      applicationExtensionBlock[11] = 50; //2
      applicationExtensionBlock[12] = 46; //.
      applicationExtensionBlock[13] = 48; //0
      applicationExtensionBlock[14] = 3; //Size of block
      applicationExtensionBlock[15] = 1; //data sub-block index (always 1)
      applicationExtensionBlock[16] = 0; //unsigned number of repetitions (low byte) ???
      applicationExtensionBlock[17] = 0; //unsigned number of repetitions (high byte) ???
      applicationExtensionBlock[18] = 0; //Block terminator

      _binaryWriter.Write(applicationExtensionBlock, 0, 19); //Application extension
    }

    private void WriteFrame(Image frame, bool writeHeader)
    {
      using (var memoryStream = new MemoryStream())
      {
        frame.Save(memoryStream, _imageCodecInfo, _encoderParameters);
        Byte[] imageData = memoryStream.ToArray();

        if (writeHeader)
        {
          //only write these the first time....
          WriteHeaderAndColorTable(imageData);
          WriteApplicationExtensionBlock();
        }

        //_graphicControlExtensionBlock[4] = LoByte(delay);
        //_graphicControlExtensionBlock[5] = HiByte(delay);

        _binaryWriter.Write(_graphicControlExtensionBlock, 0, 8); //Graphic extension
        _binaryWriter.Write(imageData, 789, imageData.Length - 790); //Image data
      }
    }

    private void WriteImageTerminator()
    {
      //only write this one the last time....
      _binaryWriter.Write(";"); //Image terminator
    }

    private static byte LoByte(short value)
    {
      return Convert.ToByte(value & 0xff);
    }

    private static byte HiByte(short value)
    {
      return Convert.ToByte((value >> 8) & 0xff);
    }
  }
}